<template>
  <div>
    <a-row>
      <a-col :span="6">
        <a-space direction="vertical" style="width: 100%">
          <a-alert :message="wsInfo.msg" :type="wsInfo.type" show-icon />
          <template v-if="displayData.id">
            <a-collapse
              :activeKey="displayData.activeKey"
              :bordered="false"
              default-active-key="1"
              style="width: 100%"
            >
              <template #expandIcon="props">
                <a-icon :rotate="props.isActive ? 90 : 0" type="caret-right" />
              </template>
              <a-collapse-panel key="0" :style="customStyle" header="标识符">
                {{ displayData.id }}
              </a-collapse-panel>
              <a-collapse-panel key="1" :style="customStyle" header="类别">
                {{ displayData.cate }}
              </a-collapse-panel>
              <a-collapse-panel key="2" :style="customStyle" header="属性">
                <a-table
                  :columns="displayData.columns"
                  :data-source="displayData.prop"
                  :pagination="displayData.pagination"
                  style="width: 100%"
                >
                  <span slot="val" slot-scope="val">
                    <a-popover title="值详情">
                      {{ val }}
                      <template slot="content">
                        {{ val }}
                      </template>
                    </a-popover>
                  </span>
                </a-table>
              </a-collapse-panel>
              <a-collapse-panel
                v-if="displayData.desc !== ''"
                key="-1"
                :style="customStyle"
                header="描述"
              >
                {{ displayData.desc }}
              </a-collapse-panel>
              <a-collapse-panel key="3" :style="customStyle" header="操作">
                <a-space>
                  <a-button v-on:click="removeNode">删除节点</a-button>
                </a-space>
              </a-collapse-panel>
            </a-collapse>
          </template>
          <template v-else>
            <a-table
              :columns="logsData.columns"
              :data-source="logsData.data"
              :pagination="logsData.pagination"
              style="width: 100%"
              tableLayout="fixed"
            >
              <template slot="id" slot-scope="id">
                <small class="addText" v-on:click="add2Task(id)"> 添加 </small>
              </template>
            </a-table>
          </template>
        </a-space>
      </a-col>
      <a-col :span="1" />
      <a-col :span="17">
        <a-card :bordered="false">
          <div slot="title" style="width: 100%">
            <a-row>
              <a-col :span="20">
                <a-input-search
                  v-model="queryInfo.command"
                  :loading="queryInfo.loading"
                  placeholder="输入图名称并查询"
                  style="width: 100%"
                  @search="updateCytoscapeData"
                >
                  <a-button
                    slot="enterButton"
                    :disabled="queryInfo.command === ''"
                    type="primary"
                    >查询</a-button
                  >
                </a-input-search>
              </a-col>
              <a-col :span="1" />
              <a-col :span="3">
                <a-space>
                  <a-button icon="save" v-on:click="save"></a-button>
                  <a-button icon="undo"></a-button>
                  <a-button icon="redo"></a-button>
                </a-space>
              </a-col>
            </a-row>
          </div>
          <div id="cytoscape" style="width: 100%; height: 36vw" />
        </a-card>
      </a-col>
    </a-row>
  </div>
</template>

<script>
import cytoscape from "cytoscape";
import cola from "cytoscape-cola";

cytoscape.use(cola);
let cy = null;
let selectedTarget = null;
let currentTaskNode = null;
export default {
  name: "Dashboard",
  data() {
    return {
      customStyle:
          "background: #f7f7f7;border-radius: 4px;margin-bottom: 24px;border: 0;overflow: hidden",
      websocket: null,
      socketAddr: "ws://s.edgeless.top:18080/ws",
      //socketAddr: "ws://localhost:3000/ws",
      wsInfo: {
        type: "warning",
        msg: "等待WebSocket握手...",
      },
      cy: null,
      queryInfo: {
        command: "",
        loading: false,
      },
      iconTable: {
        UserInfo: "identity",
        Task: "sighting",
        DiamondCoreEvent: "malware",
        MetaFeature: "observed-data",
        Infrastructure: "intrusion-set",
        Victim: "vulnerability",
        Capability: "attack-pattern",
        AttackTechnique: "tool",
        Reference: "relationship",
        Service: "campaign",
        Adversary: "threat-actor",
        SocialPolitical: "report",
      },
      titleTable: {
        UserInfo: "Name",
        Task: "TaskName",
        DiamondCoreEvent: "id",
        MetaFeature: "Resources",
        Infrastructure: "IPAddrs",
        Victim: "IPAddrs",
        Capability: "id",
        AttackTechnique: "Name",
        Reference: "Refsource",
        Service: "Port",
        Adversary: "Names",
        SocialPolitical: "Descriptions",
      },
      displayData: {
        target: null,
        id: null,
        cate: "null",
        prop: [
          {
            key: "Key1",
            val: "Val1",
          },
        ],
        columns: [
          {
            title: "键",
            dataIndex: "key",
            key: "key",
          },
          {
            title: "值",
            key: "val",
            dataIndex: "val",
            ellipsis: true,
            scopedSlots: { customRender: "val" },
          },
        ],
        pagination: {
          pageSize: 3,
        },
        activeKey: ["0", "1", "2", "3"],
        desc: "23333333",
      },
      logsData: {
        data: [
          {
            id: "0xfee01",
            type: "TELNET",
            node: "远程",
            ip: "ab3d:1103:c3a2:d2db:2815:9832:bb3:68b4",
            position: "俄罗斯 俄罗斯",
            time: "23:10:41",
          },
          {
            id: "0xfee02",
            type: "HTTP",
            node: "远程",
            ip: "58b2:aa14:8b59:8b6:ceb6:107:84cc:fad8",
            position: "中国 浙江",
            time: "23:10:41",
          },
          {
            id: "0xfee03",
            type: "HTTP",
            node: "远程",
            ip: "58b2:aa14:8b59:8b6:ceb6:107:84cc:fad8",
            position: "中国 浙江",
            time: "23:10:41",
          },
          {
            id: "0xfee04",
            type: "HTTP",
            node: "远程",
            ip: "58b2:aa14:8b59:8b6:ceb6:107:84cc:fad8",
            position: "中国 浙江",
            time: "23:10:41",
          },
          {
            id: "0xfee05",
            type: "HTTP",
            node: "远程",
            ip: "58b2:aa14:8b59:8b6:ceb6:107:84cc:fad8",
            position: "中国 浙江",
            time: "23:10:41",
          },
          {
            id: "0xfee06",
            type: "HTTP",
            node: "远程",
            ip: "58b2:aa14:8b59:8b6:ceb6:107:84cc:fad8",
            position: "中国 浙江",
            time: "23:10:41",
          },
          {
            id: "0xfee07",
            type: "HTTP",
            node: "远程",
            ip: "58b2:aa14:8b59:8b6:ceb6:107:84cc:fad8",
            position: "中国 浙江",
            time: "23:10:41",
          },
          {
            id: "0xfee08",
            type: "HTTP",
            node: "远程",
            ip: "58b2:aa14:8b59:8b6:ceb6:107:84cc:fad8",
            position: "中国 浙江",
            time: "23:10:41",
          },
          {
            id: "0xfee09",
            type: "HTTP",
            node: "远程",
            ip: "58b2:aa14:8b59:8b6:ceb6:107:84cc:fad8",
            position: "中国 浙江",
            time: "23:10:41",
          },
          {
            id: "0xfee10",
            type: "HTTP",
            node: "远程",
            ip: "58b2:aa14:8b59:8b6:ceb6:107:84cc:fad8",
            position: "中国 浙江",
            time: "23:10:41",
          },
          {
            id: "0xfee11",
            type: "HTTP",
            node: "远程",
            ip: "58b2:aa14:8b59:8b6:ceb6:107:84cc:fad8",
            position: "中国 浙江",
            time: "23:10:41",
          },
        ],
        columns: [
          {
            title: "类型",
            dataIndex: "type",
            key: "type",
            ellipsis: true,
          },
          {
            title: "IP",
            dataIndex: "ip",
            key: "ip",
            ellipsis: true,
          },
          {
            title: "地区",
            dataIndex: "position",
            key: "position",
            ellipsis: true,
          },
          {
            title: "攻击时间",
            dataIndex: "time",
            key: "time",
            ellipsis: true,
          },
          {
            title: "操作",
            key: "id",
            dataIndex: "id",

            scopedSlots: { customRender: "id" },
          },
        ],
        pagination: {
          pageSize: 10,
        },
      },
    };
  },
  methods: {
    initCytoscape() {
      let dom = document.getElementById("cytoscape");
      cy = cytoscape({
        container: dom,
        elements: {
          "nodes": [
            {
              "data": {
                "CreateTime": "2021-05-06T06:38:51+08:00",
                "TaskName": "任务",
                "id": "0x1053b1",
                "parent": ""
              },
              "classes": "Task"
            },
            {
              "data": {
                "CreateTime": "2021-04-02T00:18:12+08:00",
                "Name": "用户信息",
                "id": "0xfde83",
                "parent": ""
              },
              "classes": "UserInfo"
            },
            {"data": {"id": "钻石核心事件"}, "classes": "DiamondCoreEvent"},
            {
              "data": {
                "CreateTime": "2021-05-06T06:38:51+08:00",
                "DataSources": null,
                "Detections": ["2027063"],
                "Direction": "",
                "Methodologies": ["suricata"],
                "ModifyTime": "2021-05-06T06:40:02+08:00",
                "Phases": [
                  "lateral-movement",
                  "persistence",
                  "privilege-escalation",
                  "defense-evasion",
                  "collection",
                  "credential-access"
                ],
                "Resources": "元特征",
                "ThreatScore": 8.333333,
                "Timestamp": "2021-04-19T04:58:10+08:00",
                "id": "0x1053b2",
                "parent": ""
              },
              "classes": "MetaFeature"
            },
            {
              "data": {
                "ASNs": null,
                "AttackPaths": null,
                "ContactEmails": null,
                "ContactPhones": null,
                "Domains": null,
                "GeoLocation": "科索沃/PTK",
                "Hostnames": null,
                "ICPName": "",
                "ICPNature": "",
                "ICPNumber": "",
                "ICPSitename": "",
                "IPAddrs": "基础设施",
                "ISP": "",
                "OS": "",
                "Organization": "",
                "Registrant": "",
                "Routes": null,
                "Types": ["adversary"],
                "id": "0x1053b4",
                "parent": ""
              },
              "classes": "Infrastructure"
            },
            {
              "data": {
                "ASNs": ["AS45090"],
                "AttackPaths": null,
                "ContactEmails": null,
                "ContactPhones": null,
                "Domains": null,
                "GeoLocation": "上海市/腾讯云",
                "Hostnames": null,
                "ICPName": "",
                "ICPNature": "",
                "ICPNumber": "",
                "ICPSitename": "",
                "IPAddrs": "受害者",
                "ISP": "Shenzhen Tencent Computer Systems Company Limited",
                "OS": "",
                "Organization": "Tencent cloud computing (Beijing) Co., Ltd.",
                "Registrant": "",
                "Routes": null,
                "Types": ["victim"],
                "id": "0x1053b5",
                "parent": ""
              },
              "classes": "Victim"
            },
            {
              "data": {
                "Banner": "OpenSSH",
                "Port": "服务",
                "Version": "8.2p1 Ubuntu-4ubuntu0.2",
                "id": "0x1053b6",
                "parent": ""
              },
              "classes": "Service"
            },
            {
              "data": {
                "Banner": "OpenSSH",
                "Port": "服务",
                "Version": "7.6p1 Ubuntu-4ubuntu0.3",
                "id": "0x1053b7",
                "parent": ""
              },
              "classes": "Service"
            },
            {
              "data": {
                "Banner": "nginx",
                "Port": "服务",
                "Version": "1.14.0",
                "id": "0x1053b8",
                "parent": ""
              },
              "classes": "Service"
            },
            {
              "data": {"ExtractedFilePaths": null, "id": "能力", "parent": ""},
              "classes": "Capability"
            },
            {
              "data": {
                "CreateTime": "2017-05-31T21:30:26Z",
                "Descriptions": [
                  "Adversaries may use rootkits to hide the presence of programs, files, network connections, services, drivers, and other system components. Rootkits are programs that hide the existence of malware by intercepting/hooking and modifying operating system API calls that supply system information. (Citation: Symantec Windows Rootkits) \n\nRootkits or rootkit enabling functionality may reside at the user or kernel level in the operating system or lower, to include a hypervisor, Master Boot Record, or [System Firmware](https://attack.mitre.org/techniques/T1542/001). (Citation: Wikipedia Rootkit) Rootkits have been seen for Windows, Linux, and Mac OS X systems. (Citation: CrowdStrike Linux Rootkit) (Citation: BlackHat Mac OSX Rootkit)"
                ],
                "ModifyTime": "2020-06-20T22:29:55Z",
                "Name": "攻击方式",
                "NameFull": "Rootkit",
                "Phases": ["defense-evasion"],
                "Platforms": null,
                "Url": "https://attack.mitre.org/techniques/T1014",
                "id": "0xf9a03",
                "parent": ""
              },
              "classes": "AttackTechnique"
            },
            {
              "data": {
                "Descriptions": [
                  "Kurtz, G. (2012, November 19). HTTP iframe Injecting Linux Rootkit. Retrieved December 21, 2017."
                ],
                "Name": "",
                "Refsource": "引用",
                "Tags": null,
                "Url": "https://www.crowdstrike.com/blog/http-iframe-injecting-linux-rootkit/",
                "id": "0xf9a1a",
                "parent": ""
              },
              "classes": "Reference"
            },
            {
              "data": {
                "Descriptions": [
                  "Wikipedia. (2016, June 1). Rootkit. Retrieved June 2, 2016."
                ],
                "Name": "",
                "Refsource": "引用",
                "Tags": null,
                "Url": "https://en.wikipedia.org/wiki/Rootkit",
                "id": "0xf9a33",
                "parent": ""
              },
              "classes": "Reference"
            },
            {
              "data": {
                "Descriptions": [
                  "Symantec. (n.d.). Windows Rootkit Overview. Retrieved December 21, 2017."
                ],
                "Name": "",
                "Refsource": "引用",
                "Tags": null,
                "Url": "https://www.symantec.com/avcenter/reference/windows.rootkit.overview.pdf",
                "id": "0xf9a4a",
                "parent": ""
              },
              "classes": "Reference"
            },
            {
              "data": {
                "Descriptions": [
                  "Pan, M., Tsai, S. (2014). You can’t see me: A Mac OS X Rootkit uses the tricks you haven't known yet. Retrieved December 21, 2017."
                ],
                "Name": "",
                "Refsource": "引用",
                "Tags": null,
                "Url": "http://www.blackhat.com/docs/asia-14/materials/Tsai/WP-Asia-14-Tsai-You-Cant-See-Me-A-Mac-OS-X-Rootkit-Uses-The-Tricks-You-Havent-Known-Yet.pdf",
                "id": "0xf9ab0",
                "parent": ""
              },
              "classes": "Reference"
            }
          ],
          "edges": [
            {
              "data": {
                "id": "0x1053b10xfde83",
                "source": "0x1053b1",
                "target": "0xfde83"
              }
            },
            {
              "data": {
                "id": "0x1053b1钻石核心事件",
                "source": "0x1053b1",
                "target": "钻石核心事件"
              }
            },
            {"data": {"id": "钻石核心事件", "source": "钻石核心事件", "target": ""}},
            {
              "data": {
                "id": "钻石核心事件0x1053b2",
                "source": "钻石核心事件",
                "target": "0x1053b2"
              }
            },
            {
              "data": {
                "id": "0x1053b20xfde83",
                "source": "0x1053b2",
                "target": "0xfde83"
              }
            },
            {
              "data": {
                "id": "钻石核心事件0x1053b4",
                "source": "钻石核心事件",
                "target": "0x1053b4"
              }
            },
            {
              "data": {
                "id": "0x1053b50x1053b6",
                "source": "0x1053b5",
                "target": "0x1053b6"
              }
            },
            {
              "data": {
                "id": "0x1053b50x1053b7",
                "source": "0x1053b5",
                "target": "0x1053b7"
              }
            },
            {
              "data": {
                "id": "0x1053b50x1053b8",
                "source": "0x1053b5",
                "target": "0x1053b8"
              }
            },
            {
              "data": {
                "id": "钻石核心事件0x1053b5",
                "source": "钻石核心事件",
                "target": "0x1053b5"
              }
            },
            {
              "data": {
                "id": "0xf9a030xf9a1a",
                "source": "0xf9a03",
                "target": "0xf9a1a"
              }
            },
            {
              "data": {
                "id": "0xf9a030xf9a33",
                "source": "0xf9a03",
                "target": "0xf9a33"
              }
            },
            {
              "data": {
                "id": "0xf9a030xf9a4a",
                "source": "0xf9a03",
                "target": "0xf9a4a"
              }
            },
            {
              "data": {
                "id": "0xf9a030xf9ab0",
                "source": "0xf9a03",
                "target": "0xf9ab0"
              }
            },
            {
              "data": {
                "id": "能力0xf9a03",
                "source": "能力",
                "target": "0xf9a03"
              }
            },
            {
              "data": {
                "id": "钻石核心事件能力",
                "source": "钻石核心事件",
                "target": "能力"
              }
            }
          ]
        },
        style: [
          // the stylesheet for the graph
          {
            selector: "node",
            style: {
              "background-color": "#666",
              label: "data(id)",
            },
          },

          {
            selector: "edge",
            style: {
              width: 3,
              "line-color": "#ccc",
              "target-arrow-color": "#ccc",
              "target-arrow-shape": "triangle",
              "curve-style": "bezier",
            },
          },
        ],
        minZoom: 0.5,
        maxZoom: 5,
        wheelSensitivity: 0.1,
      });
      // this.generateInfo(cy.nodes()[0])
      this.replaceNodeImage();
      this.replaceNodeTitle();
      this.findTaskNode(cy.nodes());
      cy.layout({
        name: "cola",
      }).run();
      cy.on("click", this.onClickElem);
    },
    initSocket() {
      this.websocket = new WebSocket(this.socketAddr);
      this.websocket.onopen = this.wsOpen;
      this.websocket.onerror = this.wsError;
      this.websocket.onclose = this.wsClose;
      this.websocket.onmessage = this.wsMessage;
    },
    save() {
      let dataURLtoBlob = function (dataurl) {
        let arr = dataurl.split(','),
            mime = arr[0].match(/:(.*?);/)[1],
            bstr = atob(arr[1]),
            n = bstr.length,
            u8arr = new Uint8Array(n);
        while (n--) {
          u8arr[n] = bstr.charCodeAt(n);
        }
        return new Blob([u8arr], {type: mime});
      }

      let blob = dataURLtoBlob(cy.png())
      let link = document.createElement('a')
      link.download = 'export.png'
      link.href = URL.createObjectURL(blob)
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
    },

    wsOpen() {
      this.wsInfo = {
        type: "success",
        msg: "WebSocket已连接",
      };
    },
    wsError(e) {
      this.wsInfo = {
        type: "error",
        msg: "WebSocket连接出错：" + e.toString(),
      };
    },
    wsClose() {
      this.wsInfo = {
        type: "error",
        msg: "WebSocket连接已关闭",
      };
    },
    wsMessage(e) {
      let json = JSON.parse(e.data)
      console.log(json)
      switch (json.type) {
        case "get_task":
          console.log("get_task_response")
          //使用接收的数据更新图
          cy.json(JSON.parse(json.payload))
          //选中第一个节点，显示属性并展开
          // this.generateInfo(cy.nodes()[0])
          //替换节点显示
          this.replaceNodeImage()
          this.replaceNodeTitle()
          //查找任务节点
          this.findTaskNode(cy.nodes())
          //应用排版
          cy.layout({ name: "cola" }).run()

          this.queryInfo.loading = false
          this.queryInfo.command = ""
          this.displayData.id = null

          break;
        case "update_logs":
          break
      }
    },
    wsSend(socket, json) {
      let nws = new WebSocket(this.socketAddr + socket)
      nws.onopen = function () {
        nws.send(JSON.stringify(json))
        nws.close()
      }
    },
    wsSendWithResponse(socket, json, callback) {
      let nws = new WebSocket(this.socketAddr + socket)
      nws.onopen = function () {
        nws.send(JSON.stringify(json))
      }
      nws.onmessage = function (e) {
        let res = JSON.parse(e.data)
        console.log(res)
        callback(res)
        nws.close()
      }
    },


    onClickElem(event) {
      let t = event.target;
      if (!t.length) {
        //取消高亮上一次选中的图标
        if (selectedTarget) selectedTarget.style("border-width", "0px");
        //清空显示信息
        selectedTarget = null;
        this.displayData.id = null;

        return;
      }
      if (t.isNode()) {
        //更新显示信息
        this.generateInfo(t);
      }
    },
    generateInfo(t) {
      //取消高亮上一次选中的图标
      if (selectedTarget) selectedTarget.style("border-width", "0px");
      //将此次的元素高亮
      t.style("border-color", "#1565c0");
      t.style("border-width", "3px");
      //更新target
      selectedTarget = t;

      let data = this.getData(t);
      let cate = this.getClass(t);

      //更新id
      this.displayData.id = data["id"];
      //更新分类
      this.displayData.cate = cate;
      //更新参数栏数据
      this.displayData.prop = [];
      this.displayData.desc = "";
      for (let i in data) {
        //处理值为空的情况
        if (!data[i]) data[i] = "null";

        //分离description
        if (i === "Descriptions" && data[i] !== "null") {
          //console.log(data[i])
          if (data[i].length) {
            data[i].forEach((item) => {
              this.displayData.desc += item;
            });
          } else {
            this.displayData.desc = data[i];
          }
        } else {
          this.displayData.prop.push({
            key: i,
            val: data[i],
          });
        }
      }
      //console.log(this.displayData.prop)
    },
    findTaskNode(nodes) {
      currentTaskNode = null;
      for (let i = 0; i < nodes.length; i++) {
        if (this.getClass(nodes[i]) === "Task") {
          currentTaskNode = nodes[i];
          break;
        }
      }
      console.log(currentTaskNode);
    },

    getClass(t) {
      return t.classes()[0];
    },
    getData(t) {
      return t.data();
    },
    setSvg(t, value) {
      t.css({
        "background-image": "/stix/" + value + ".svg",
        "background-height": 30,
        "background-width": 30,
      });
    },
    setTitle(t, value) {
      t.style("content", value);
    },

    updateCytoscapeData() {
      let cmd = this.queryInfo.command;
      if (!cmd) return;
      this.queryInfo.loading = true;
      //发送请求
      this.wsSendWithResponse("Query", {
        type: "get_task",
        payload: {
          taskName: this.queryInfo.command
        }
      }, (json) => {
        console.log("get")
        console.log(json)
        //使用接收的数据更新图
        cy.json(json.payload)
        //选中第一个节点，显示属性并展开
        // this.generateInfo(cy.nodes()[0])
        //替换节点显示
        this.replaceNodeImage()
        this.replaceNodeTitle()
        //查找任务节点
        this.findTaskNode(cy.nodes())
        //应用排版
        cy.layout({name: "cola"}).run()

        this.queryInfo.loading = false
        this.queryInfo.command = ""
        this.displayData.id = null
      });
    },
    replaceNodeImage() {
      let nodes = cy.nodes();
      for (let i = 0; i < nodes.length; i++) {
        this.setSvg(nodes[i], this.iconTable[this.getClass(nodes[i])]);
      }
    },
    replaceNodeTitle() {
      let nodes = cy.nodes();
      for (let i = 0; i < nodes.length; i++) {
        let propName = this.titleTable[this.getClass(nodes[i])];
        let nodeData = this.getData(nodes[i]);
        let nodeTitle = nodeData[propName];
        if (!nodeTitle) nodeTitle = "null";
        this.setTitle(nodes[i], nodeTitle);
      }
    },

    add2Task(id) {
      if (this.getData(currentTaskNode)["TaskName"] === "任务") {
        this.$message.error('无法添加任务到图例，请先查询新的图名称后再添加');
        return
      }
      console.log("add2task for " + id);

      //更改图
      cy.add([
        {
          group: "nodes",
          data: {
            id,
          },
          classes: "DiamondCoreEvent",
        },
        {
          group: "edges",
          data: {
            id: "e0" + id,
            source: this.getData(currentTaskNode)["id"],
            target: id,
          },
        },
      ]);

      //发送图
      this.wsSend("Query", {
        type: "add_task",
        payload: {
          taskName: this.getData(currentTaskNode)["TaskName"],
          logId: id
        }
      })

      //刷新图标
      this.replaceNodeImage();
      this.replaceNodeTitle();
      cy.layout({name: "cola"}).run();
    },
    removeNode() {
      if (this.getClass(selectedTarget) === "Task") {
        this.$message.error("禁止删除Task节点");
        return;
      }
      this.displayData.id = null;
      selectedTarget.remove();
    },
  },
  mounted() {
    this.initCytoscape();
    this.initSocket();
  },
  destroyed() {
    this.websocket.close();
  },
};
</script>

<style>
.addText {
  color: #2196f3;
}

.addText:hover {
  cursor: pointer;
}
</style>